﻿#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

using Patterns.Collections;
using Patterns.Text;
using Patterns.Validation.Formatting;

namespace Patterns.Validation
{
	/// <summary>
	/// 	Provides arbitrary validation via a fluent interface.
	/// </summary>
	public class Verification
	{
		/// <summary>
		/// 	Represents the phrase that can be used in <see cref = "FailWith" /> as a placeholder for the reason of an assertion.
		/// </summary>
		public const string ReasonTag = "{reason}";

		#region Private Definitions

		[ThreadStatic] private static string _subjectName;
		private string _reason;
		private bool _succeeded;
		private bool _useLineBreaks;

		#endregion

		/// <summary>
		/// 	Initializes a new instance of the <see cref = "Verification" /> class.
		/// </summary>
		internal Verification() {}

		/// <summary>
		/// 	Gets or sets the name of the subject for the next verification.
		/// </summary>
		public static string SubjectName
		{
			get { return _subjectName; }
			set { _subjectName = value; }
		}

		/// <summary>
		/// 	Indicates that every argument passed into <see cref = "FailWith" /> is displayed on a separate line.
		/// </summary>
		public Verification UsingLineBreaks
		{
			get
			{
				_useLineBreaks = true;
				return this;
			}
		}

		/// <summary>
		/// 	Gets the name or identifier of the current subject, or a default value if the subject is not known.
		/// </summary>
		public static string SubjectNameOr(string defaultName)
		{
			return string.IsNullOrEmpty(SubjectName) ? defaultName : SubjectName;
		}

		/// <summary>
		/// 	Specifies the condition that must be satisfied.
		/// </summary>
		/// <param name = "condition">If <c>true</c> the verification will be succesful.</param>
		public Verification ForCondition(bool condition)
		{
			_succeeded = condition;
			return this;
		}

		/// <summary>
		/// 	Specifies a predicate representing the condition that must be satisfied.
		/// </summary>
		public Verification ForCondition(Func<bool> condition)
		{
			_succeeded = condition();
			return this;
		}

		/// <summary>
		/// 	Specifies the reason why you expect the condition to be <c>true</c>.
		/// </summary>
		/// <param name = "reason">
		/// 	A formatted phrase explaining why the condition should be satisfied. If the phrase does not 
		/// 	start with the word <i>because</i>, it is prepended to the message.
		/// </param>
		/// <param name = "reasonArgs">
		/// 	Zero or more values to use for filling in any <see cref = "string.Format(string,object[])" /> compatible placeholders.
		/// </param>
		public Verification BecauseOf(string reason, params object[] reasonArgs)
		{
			_reason = SanitizeReason(reason, reasonArgs);
			return this;
		}

		/// <summary>
		/// 	Define the failure message for the verification.
		/// </summary>
		/// <remarks>
		/// 	If the <paramref name = "failureMessage" /> contains the text "{reason}", this will be replaced by the reason as
		/// 	defined through <see cref = "BecauseOf" />.
		/// </remarks>
		/// <param name = "failureMessage">The format string that represents the failure message.</param>
		/// <param name = "failureArgs">Optional arguments for the <paramref name = "failureMessage" /></param>
		public void FailWith(string failureMessage, params object[] failureArgs)
		{
			try
			{
				if (!_succeeded)
				{
					string message = ReplaceReasonTag(failureMessage);
					string exceptionMessage = BuildExceptionMessage(message, failureArgs);

					throw new Exception(exceptionMessage);
				}
			}
			finally
			{
				_succeeded = false;
			}
		}

		private string ReplaceReasonTag(string failureMessage)
		{
			return !string.IsNullOrEmpty(_reason)
			       	? ReplaceReasonTagWithFormatSpecification(failureMessage)
			       	: failureMessage.Replace(ReasonTag, string.Empty);
		}

		private static string ReplaceReasonTagWithFormatSpecification(string failureMessage)
		{
			if (!failureMessage.Contains(ReasonTag))
			{
				throw new InvalidOperationException(
					@"Reason is specified through 'BecauseOf(...)', but format string does not contain a placeholder for the reason!");
			}

			string message = IncrementAllFormatSpecifiers(failureMessage);
			message = message.Replace(ReasonTag, "{0}");

			return message;
		}

		private static string SanitizeReason(string reason, object[] reasonArgs)
		{
			if (!string.IsNullOrEmpty(reason))
			{
				if (!reason.StartsWith("because", StringComparison.CurrentCultureIgnoreCase))
					reason = "because " + reason;

				return " " + String.Format(reason, reasonArgs);
			}

			return string.Empty;
		}

		private static string IncrementAllFormatSpecifiers(string message)
		{
			CompiledRegex specifierPattern = @"\{(?<specifier>\d+)\}";

			specifierPattern.Matches(message).OfType<Match>().Each(match =>
			{
				int index = Convert.ToInt32(match.Groups["specifier"].Value);
				int newIndex = index + 1;
				string oldTag = "{" + index + "}";
				string newTag = "{" + newIndex + "}";
				message = message.Replace(oldTag, newTag);
			});

			return message;
		}

		private string BuildExceptionMessage(string failureMessage, IEnumerable<object> failureArgs)
		{
			var values = new List<string>();
			if (!string.IsNullOrEmpty(_reason))
				values.Add(_reason);

			values.AddRange(failureArgs.Select(a => _useLineBreaks ? Formatter.ToStringLine(a) : Formatter.ToString(a)));

			return string.Format(failureMessage, values.ToArray()).Replace("{{{{", "{{").Replace("}}}}", "}}");
		}
	}
}